package com.study.sample.service.impl;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.study.sample.entity.Document;
import com.study.sample.entity.dto.DocumentDTO;
import com.study.sample.entity.vo.DocumentVO;
import com.study.sample.service.DocumentService;
import com.study.sample.utils.DateParseUtil;
import com.study.sample.utils.EsClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;

@Service
@Slf4j
public class DocumentServiceImpl implements DocumentService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    //存储游标的集合
    private static final Map<String, Map<Integer, Object[]>> sortMap = new HashMap<>(256);

    @Override
    public DocumentVO deepSearchPage(DocumentDTO documentDTO, HttpServletRequest req) {
        String id = req.getSession().getId();
        //条件构造器
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //返回的数据
        List<Document> documents = new ArrayList<>();
        //当前页
        int currentPageNum;
        //总数
        long total = 0;

        DocumentVO documentVO = new DocumentVO();

        if (StrUtil.isEmpty(id)) throw new RuntimeException("id不能为空");

        //页码和游标对应的集合
        Map<Integer, Object[]> pageMap = sortMap.get(id);

        //---------------设置查询条件start--------------
        //范围查询
        if (documentDTO.getStartTime() != null) {
            Date startDate = DateParseUtil.parseString2Date(documentDTO.getStartTime());
            boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").gte(startDate));
        }
        if (documentDTO.getEndTime() != null) {
            Date endDate = DateParseUtil.parseString2Date(documentDTO.getEndTime());
            boolQueryBuilder.filter(QueryBuilders.rangeQuery("createTime").lte(endDate));
        }
        // 模糊查询
        if (documentDTO.getContent() != null) {
            // 同一字段在多个field里查询
            // boolQueryBuilder.filter(QueryBuilders.multiMatchQuery(documentDTO.getContent(), fields));

            boolQueryBuilder.must((QueryBuilders.wildcardQuery("content", documentDTO.getContent())));
        }
        if (documentDTO.getTitle() != null) {
            boolQueryBuilder.should((QueryBuilders.wildcardQuery("title", documentDTO.getTitle())));
        }
        //---------------设置查询条件end----------------

        /*首先不能是第一页，其次页码集合不能是空的，对应的页码也得在这个集合里，最后是当前页要小于此集合。
         * 我觉得重点在于最后一条，为什么一定要小于呢？因为当前页数=集合的容量，可以视为已经翻到了最后一页，那么我们要继续向后查询5页
         * 索引所以我们把这个边界处理放到了最后一层，本层只处理缓存有的游标，存在就放search after里查*/
        if (documentDTO.getPageNum() != 1 && MapUtil.isNotEmpty(pageMap)
                && pageMap.containsKey(documentDTO.getPageNum())
                && pageMap.size() > documentDTO.getPageNum()) {
            try {
                //构造查询条件
                searchSourceBuilder.query(boolQueryBuilder)
                        .sort("_id", SortOrder.DESC) //拿什么排序，游标就是什么
                        .size(documentDTO.getPageSize());
                //从缓存里拿到了当前页的游标---> 存放的时候就已经做了对应处理！！
                searchSourceBuilder.searchAfter(pageMap.get(documentDTO.getPageNum()));
                SearchRequest searchRequest2 = new SearchRequest("document")
                        .source(searchSourceBuilder);
                SearchResponse searchResponse2 = restHighLevelClient.search(searchRequest2, RequestOptions.DEFAULT);
                SearchHits searchHits = searchResponse2.getHits();
                if (searchHits.getTotalHits().value > 0) {
                    SearchHit[] hits = searchHits.getHits();
                    EsClientUtil.convertResult(documents, Document.class, hits);
                    total = searchHits.getTotalHits().value;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        /*当pageNum=1的时候我就默认他刚接在或者已经刷新当然也有可能是从第2页回去之类的情况，但这里均不予考虑，只要是1就
         * 重新构造页标和游标的对应关系*/
        else if (documentDTO.getPageNum() == 1) {
            // 先移除
            sortMap.remove(id);
            // 上面被移除，pageMap更不可能获取到，这里必须自己初始化
            pageMap = new HashMap<>();
            //游标
            Object[] sortValues;
            //当前页
            currentPageNum = 1;
            //下一页
            int nextPageNum = currentPageNum + 1;

            try {
                searchSourceBuilder.query(boolQueryBuilder)
                        .sort("_id", SortOrder.DESC)
                        .from(0) //必须是0，不熟悉的朋友可能会觉得这里就可以循环，from从1开始就可以拿第二页，其实不行
                        //这样拿到的数据会有一点点错位，而easy-es这个框架是直接不允许深分页查询from > 0的
                        .size(documentDTO.getPageSize());
                SearchRequest searchRequest = new SearchRequest("document").source(searchSourceBuilder);
                SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                SearchHit[] hits = searchResponse.getHits().getHits();
                if (hits.length != 0) {
                    //查询最后一个数据
                    SearchHit result = hits[hits.length - 1];
                    sortValues = result.getSortValues();
                    pageMap.put(1, new Object[]{}); // 第一页没有游标
                    pageMap.put(2, sortValues); //第一页的游标是去拿第二页的数据的，所以是2
                    EsClientUtil.convertResult(documents, Document.class, hits);
                    total = searchResponse.getHits().getTotalHits().value;
                }

                //向后获取5页的游标数据 所以你要品nextPageNum和currentPageNum的作用，就是处理游标和页码的对应关系的
                for (int i = nextPageNum; i < nextPageNum + 5; i++) {
                    //取出上一页的游标
                    searchSourceBuilder.searchAfter(pageMap.get(i));
                    SearchRequest searchRequest2 = new SearchRequest("document")
                            .source(searchSourceBuilder);
                    SearchResponse searchResponse2 = restHighLevelClient.search(searchRequest2, RequestOptions.DEFAULT);
                    SearchHits searchHits = searchResponse2.getHits();
                    if (searchHits.getTotalHits().value > 0) {
                        SearchHit[] nextHits = searchHits.getHits();
                        //当数据量不大的情况下且每页pageSize很大的话，他可能都没有5页，所以每次循环要判断，一旦
                        //不足就要终止,因为总数据已经不足分页了,在遍历就越界了
                        if (nextHits.length < documentDTO.getPageSize()) break;
                        SearchHit nextHit = nextHits[nextHits.length - 1];
                        sortValues = nextHit.getSortValues();
                        //从3开始 3/4/5/6/7
                        pageMap.put(i + 1, sortValues);
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
        /*这里是边界，也就是当前端页面显示当前展示的最大页数到第7页了，而你的页码正好是7，那么就该继续向后拿后面的游标并且要和页码对应*/
        else if (pageMap.containsKey(documentDTO.getPageNum()) && pageMap.size() == documentDTO.getPageNum()) {
            searchSourceBuilder.query(boolQueryBuilder)
                    .sort("_id", SortOrder.DESC)
                    .size(documentDTO.getPageSize());
            currentPageNum = documentDTO.getPageNum();
            try {
                for (int i = currentPageNum; i < currentPageNum + 5; i++) {
                    //这里要知道当前页的游标在上面的集合里已经有了
                    searchSourceBuilder.searchAfter(pageMap.get(i));
                    SearchRequest searchRequest2 = new SearchRequest("document")
                            .source(searchSourceBuilder);
                    SearchResponse searchResponse2 = restHighLevelClient.search(searchRequest2, RequestOptions.DEFAULT);
                    SearchHits searchHits = searchResponse2.getHits();
                    total = searchHits.getTotalHits().value;

                    if (searchHits.getTotalHits().value > 0) {
                        SearchHit[] hits = searchHits.getHits();
                        //这里是数据边界的终止,上面已说
                        if (hits.length < documentDTO.getPageSize()) {
                            EsClientUtil.convertResult(documents, Document.class, hits);
                            break;
                        }
                        SearchHit result = hits[hits.length - 1];
                        Object[] sortValues = result.getSortValues();
                        //存放游标
                        pageMap.put(i + 1, sortValues);
                        //这里是拿出当前页的数据也就是边界数据
                        if (i == documentDTO.getPageNum()) {
                            EsClientUtil.convertResult(documents, Document.class, hits);
                        }
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        documentVO.setPageNum(documentDTO.getPageNum());
        documentVO.setPageSize(documentDTO.getPageSize());
        documentVO.setTotal(total);
        documentVO.setData(documents);
        sortMap.put(id, pageMap);
        return documentVO;
    }
}
